package org.afk.sisy.db;

import java.nio.file.*;
import java.nio.file.attribute.*;
import java.sql.*;
import java.util.*;

import org.afk.sisy.*;
import org.afk.sisy.io.*;

public class PathDataBase
{

	private static final String LASTMODIFIED = "LASTMODIFIED";
	private static final String SIZE = "SIZE";
	private static final String DIR = "DIR";
	private static final String HASH = "HASH";
	private static final String PATH = "PATH";
	private static final String FILES = "FILES";
	private static final String DELETED = "DELETED";

	private final SisyJob job;
	private Connection conn;

	public PathDataBase(final SisyJob job)
	{
		try
		{
			Class.forName("org.h2.Driver");
		}
		catch (final ClassNotFoundException e)
		{
			throw new RuntimeException("No Database driver found. Please make sure a h2-x.x.xxx.jar is in the classpath");
		}

		this.job = job;
	}

	public void open() throws SQLException
	{
		conn = DriverManager.getConnection("jdbc:h2:" + job.getDataBase(), "", "");
		prepareTable();

	}

	private void prepareTable() throws SQLException
	{
		getStatement().execute(
				"CREATE TABLE IF NOT EXISTS " + FILES + " (" + PATH + " VARCHAR(1024) NOT NULL PRIMARY KEY, " + HASH + " VARCHAR(100), " + DIR + " BOOL, " + DELETED
						+ " BOOL, " + SIZE + " BIGINT, " + LASTMODIFIED + " BIGINT)");
		getStatement().execute("ALTER TABLE " + FILES + " ADD IF NOT EXISTS " + DELETED + " BOOL");
	}

	private PreparedStatement prepareSelectPathStatement() throws SQLException
	{
		return conn.prepareStatement("SELECT * FROM " + FILES + " WHERE " + PATH + "=?");
	}

	private PreparedStatement prepareDeletedStatement() throws SQLException
	{
		return conn.prepareStatement("UPDATE " + FILES + " SET " + DELETED + "=? WHERE " + PATH + "=?");
	}

	private PreparedStatement prepareUpdateStatement() throws SQLException
	{
		return conn.prepareStatement("UPDATE " + FILES + " SET " + HASH + "=?, " + DIR + "=?, " + DELETED + "=?, " + SIZE + "=?, " + LASTMODIFIED + "=? WHERE "
				+ PATH + "=?");
	}

	private PreparedStatement prepareInsertStatement() throws SQLException
	{
		return conn.prepareStatement("INSERT INTO " + FILES + " ( " + PATH + ", " + HASH + ", " + DIR + ", " + DELETED + ", " + SIZE + ", " + LASTMODIFIED
				+ ") VALUES(?,?,?,?,?,?)");
	}

	private Statement getStatement() throws SQLException
	{
		return conn.createStatement();
	}

	public void prepareScan() throws SQLException
	{
		getStatement().execute("UPDATE " + FILES + " SET " + DELETED + "=TRUE");
	}

	public void close() throws SQLException
	{
		conn.close();
	}

	public synchronized void update(final PathWrapper next) throws SQLException
	{
		if (next.getRelativePath().equals(next.getRoot()))
			return;
		if (!executeUpdate(next))
			if (!executeInsert(next))
				throw new RuntimeException("Could not add " + next.getRelativePath() + " to database ");
	}

	private boolean executeUpdate(final PathWrapper next) throws SQLException
	{
		final PreparedStatement updateStatement = prepareUpdateStatement();
		updateStatement.setString(1, next.getHash());
		updateStatement.setBoolean(2, next.isDirectory());
		updateStatement.setBoolean(3, false);
		updateStatement.setLong(4, next.getSize());
		updateStatement.setLong(5, next.getLastModified().toMillis());
		updateStatement.setString(6, next.getRelativePath().toString());
		updateStatement.execute();
		return updateStatement.getUpdateCount() > 0;

	}

	private ResultSet executeSelect(final PathWrapper next) throws SQLException
	{
		final PreparedStatement selectStatement = prepareSelectPathStatement();
		selectStatement.setString(1, next.getRelativePath().toString());
		selectStatement.execute();
		return selectStatement.getResultSet();

	}

	private boolean executeInsert(final PathWrapper next) throws SQLException
	{
		final PreparedStatement insertStatement = prepareInsertStatement();
		insertStatement.setString(1, next.getRelativePath().toString());
		insertStatement.setString(2, next.getHash());
		insertStatement.setBoolean(3, next.isDirectory());
		insertStatement.setBoolean(4, false);
		insertStatement.setLong(5, next.getSize());
		insertStatement.setLong(6, next.getLastModified().toMillis());
		insertStatement.execute();
		return insertStatement.getUpdateCount() >= 0;
	}

	public synchronized boolean setDeleted(final String path) throws SQLException
	{
		final PreparedStatement deleteStatement = prepareDeletedStatement();
		deleteStatement.setBoolean(1, true);
		deleteStatement.setString(2, path);
		deleteStatement.execute();
		return deleteStatement.getUpdateCount() > 0;
	}

	public boolean isChanged(final PathWrapper source) throws SQLException
	{
		final ResultSet resultSet = executeSelect(source);
		if (!resultSet.first())
			return true;

		final String dbHash = resultSet.getString(HASH);
		if (dbHash != null)
			if (!Objects.equals(dbHash, source.getHash()))
				return true;
		if (!Objects.equals(resultSet.getLong(SIZE), source.getSize()))
			return true;
		if (!SisyTools.equalLastModifies(FileTime.fromMillis(resultSet.getLong(LASTMODIFIED)), source.getLastModified()))
			return true;

		return false;
	}

	public Collection<PathWrapper> selectDeleted(final Path root) throws SQLException
	{
		final PreparedStatement selectDeletedStatement = prepareSelectDeletedStatement();
		final ResultSet resultSet = selectDeletedStatement.getResultSet();
		return asPathWrapperList(resultSet, root);
	}

	private Collection<PathWrapper> asPathWrapperList(final ResultSet resultSet, final Path root) throws SQLException
	{
		Objects.requireNonNull(root);
		if (resultSet == null)
			return Collections.emptyList();
		final List<PathWrapper> result = new ArrayList<PathWrapper>();

		for (;;)
		{
			if (!resultSet.next())
				break;
			final PathWrapper pathWrapper = createPathWrapperFromResult(resultSet, root);
			result.add(pathWrapper);

		}
		return result;
	}

	private PathWrapper createPathWrapperFromResult(final ResultSet resultSet, final Path root) throws SQLException
	{
		final String path = resultSet.getString(PATH);
		final String hash = resultSet.getString(HASH);
		final Boolean directory = resultSet.getBoolean(DIR);
		final Long size = resultSet.getLong(SIZE);
		final Long lastModified = resultSet.getLong(LASTMODIFIED);
		return new PathWrapper(hash, Paths.get(path), root.resolve(path), root, lastModified, size, directory);
	}

	private PreparedStatement prepareSelectDeletedStatement() throws SQLException
	{
		return conn.prepareStatement("SELECT * FROM " + FILES + " WHERE " + DELETED + "=true");
	}
}
